Godot 4: Make a Vampire Survivors Clone (Youtube Video)
自分も同じような制作をしていたので、こちらの動画も参考にする
https://www.youtube.com/playlist?list=PLtosjGHWDab682nfZ1f6JSQ1cjap7Ieeb
完成ゲームの紹介
https://www.youtube.com/watch?v=46xm13ZaabA&list=PLtosjGHWDab682nfZ1f6JSQ1cjap7Ieeb&index=2
Part 1: Character Movement
Godot 4.0 のダウンロード、このプロジェクトで扱っている asset のダウンロード これはまだ完成していない、asset だけがあるベースのプロジェクト
これをダウンロードして、みんな作っていってねという趣旨
背景の草を敷く
Sprite2Dノードを作成し、草png をアタッチ
Region を on にして大きさを調整し、Texture - Repeat を Enabled に設定する
今回のプロジェクトでは、画面サイズは以下に設定
Width = 640
Height = 360
InputMap に、WASDキーをそれぞれ up/left/down/right Actionとして登録
キャラクターを用意する。CharacterBody2Dノードを新規作成
Sprite2Dを追加し、プレイヤーの画像をアタッチする
MotionMode を Floating に変更する
-> 知らなかった設定kidooom.icon
今回はトップダウンゲームだから、Floating に変更したのか。
移動処理を実装
normalized() 処理について
https://gyazo.com/284689a32b8ea1064f378c60cefb526c
https://gyazo.com/fb4e0c2d72ae3aa11788b0d62e80520c
斜め入力時に移動量が増えないようにしているのが使用目的
delta についての説明
今回使用している move_and_slide()では、内部で勝手に delta を使って velocity を計算しているので、delta を無視して良い
独自の移動処理を作る場合は、変化量を delta で掛け合わせないと、FPSの増減によって結果が異なってしまう
https://www.youtube.com/watch?v=BAUn-lGBXMw&list=PLtosjGHWDab682nfZ1f6JSQ1cjap7Ieeb&index=3
Part 2: Enemy A.I
Player ノードを player group に追加しておく
Enemy の CharacterBody2D ノードを作成
player に追従したいので、player への参照を保持しておく
@onready var player = get_tree().get_first_node_in_group("player")
Camera2DをPlayerノード配下において、追従するように
Player と Enemy ノードにそれぞれ Collisition を追加しておく
global_position と position の違いについて
position は、親ノードの影響を受ける相対的なもの
https://www.youtube.com/watch?v=nApFtRKaDZI&list=PLtosjGHWDab682nfZ1f6JSQ1cjap7Ieeb&index=4
Part3: Animations
プレイヤーの左右移動方向に応じて、sprite の flip_h を反転させる
プレイヤーの sprite2d の frame を、timer を使ってアニメーションさせる方法
自分は AnimatedSprite2d でアニメーションさせてるので、この方法はやっていないkidooom.icon
Enemy の向きも flip させる
direction.x の値をチェックして行う
Enemy のアニメーション
こっちは、AnimationPlayer ノードを追加し、frame に対してキーを打ってアニメーションさせている
https://www.youtube.com/watch?v=bF_-8v1zcqs&list=PLtosjGHWDab682nfZ1f6JSQ1cjap7Ieeb&index=5
Part4: HurtBoxs & HitBoxes
この回はとても参考になった。HitBoxとHurtBoxの仕組みはまだ作ってなかったので、適用しようkidooom.icon
完成プロジェクトのソースを見ると、HitBoxとHurtBoxの名前が逆になっているので、注意する。GitHubにあがっている方が間違えてる。
HitBoxの作成
HitBoxは、ダメージを与える側
damage プロパティを持つ
Area2Dノードを新規作成し、CollsiitonShape2DとTimerを追加
attack グループに追加
以下のスクリプトをアタッチする。signal hit は自分で追加したkidooom.icon
code:gd
extends Area2D
class_name HitBox
@export var damage: int = 1
@onready var collision := $CollisionShape2D
@onready var disableTimer := $DisableTimer
signal hit()
func tempdisable() -> void:
collision.call_deferred("set", "disabled", true)
if disableTimer.is_stopped():
disableTimer.start()
func _on_disable_timer_timeout() -> void:
collision.call_deferred("set", "disabled", false)
HurtBox の作成
HurtBox は、ダメージを受ける側
Area2Dノードを新規作成し、CollsiitonShape2DとTimerを追加
enum を持たせる
@export_enum("Cooldown", "HitOnce", "DisableHitBox") var HurtBoxType = 0
signal を追加
signal hurt(damage)
_on_area_entered でHitBoxが衝突してきたの処理を書く
HitBox側を、class_name HitBoxにしておけば、動画内で説明されているいくつかのチェック処理はスキップできるので、自分としては以下のように書いた
code:gd
signal hurt(damage: int)
func _on_area_entered(area: Area2D) -> void:
var hitbox := area as HitBox
if hitbox == null:
return
if not area.is_in_group("attack"):
return
match HurtBoxType:
0: # COOLDOWN
collision.call_deferred("set", "disabled", true)
disableTimer.start()
1: # HIT_ONCE
pass
hitbox.tempdisable()
hurt.emit(hitbox.damage)
hitbox.hit.emit()
func _on_disable_timer_timeout() -> void:
collision.call_deferred("set", "disabled", false)
Layer を設定しておく
Layer1: World
Layer2: Player
Layer3: Enemy
Layer4: Loot
なんかこの動画でのMaskの設定方法間違っている気がする・・・
全部 Layer と Mask の値を一緒にしている。
以下が正しいと思われる
Player HurtBox: Layer = Player、Mask = Enemy
Enemy HurtBox: Layer = Enemy、Mask = PlayerBullet(まだないので、自分で作った)
Enemy HitBox: Layer = Enemy、Mask = Player
Playerノードの子階層 に HurtBoxのシーンを追加する
「編集可能な子」を選択して、Player 向けに当たり判定の CollisionShape を設定
Layer と Mask を設定
HurtBox の hurt signal を接続して、ダメージ処理を実装する
code:gd
func _on_hurt_box_hurt(damage) -> void:
damaged(damage)
Enemy ノードの子階層に HitBox と HurtBox のシーンを追加する
HurtBoxで「編集可能な子」を選択して、Enemyが食らう側の当たり判定の CollisionShape を設定
HitBoxで「編集可能な子」を選択して、EnemyがPlayerにダメージを当てる側判定の CollisionShape を設定
それぞれ Layer と Mask を設定
HurtBox の hurt signal を接続して、ダメージ処理を実装する
code:gd
func _on_hurt_box_hurt(damage) -> void:
damaged(damage)
https://www.youtube.com/watch?v=m8sswbLIiR0&list=PLtosjGHWDab682nfZ1f6JSQ1cjap7Ieeb&index=6
Part5: Enemy Spawner
EnemySpawner ノードを新規作成する
それとは別に、Rresouceを継承した、SpawnInfo class を 作成する
敵の出現に関するプロパティを定義する
time start
time end
enemy
enemy num
enemy spawn delay
EnemySpawner スクリプトに、Array[SpawnInfo]プロパティを定義
定期的にEnemyを生成する処理を実装
プレイヤーを中心とした画面外のどこかに敵を出現させるための座標計算処理がやや複雑だけど、参考になった。
自前で EnemySpanwer 作ってはいたが、この動画ベースの方が良い感じに動くので、合わせて修正したkidooom.icon
https://www.youtube.com/watch?v=1qLIAWgySGc
Part6: Ice Spear Attack
プレイヤーの攻撃手段である Ice Spear を実装する
Area2Dノードを新規作成
Sprite2Dノードを追加
CollisionShape2Dを追加して槍の形に合わせる
Timer を追加
AudioStreamPlayer を追加
Script を追加
level, hp, speed, damage, knock_amount, attack_size のプロパティを持つ
target を決める
target に向けて angle を決めて、rotation をセット
_physics_process で移動
敵にヒット時に消えるようにする
Player ノードに、Attack関連ノードと処理を追加
IceSpear の Timer を2つ追加
1つは、IceSpear 攻撃自体のタイマーで、もう1つは、2発以上の弾数がある場合に連続発射する用のタイマー
プレイヤーの方で、攻撃ターゲットを決めて timer の timeout 時に発射する
攻撃ターゲットを決めるための、Area2Dノードをプレイヤー配下に追加
カメラ範囲を覆うぐらいの円形CollisionShape
敵のbodyが enter したら、配列に追加。exit したら、配列から削除
https://www.youtube.com/watch?v=XtlBr9KPD_g
Part7: Enemy Improvement
Player の攻撃が持つ knockback_amount を参照して、敵をノックバックさせる
武器の angle と knockback_amount を signal で受け取り、敵側の移動処理の中に knockback 方向への移動値を追加する
move_toward で徐々に knockback 値は減っていく
これまでのチュートリアル通りに作ると、武器が敵を貫通する場合、ノックバックで2回命中してしまう不具合が起こる
そこで、敵の Hurtbox type を Cooldown -> HitOnce に変更
HitOnce の分岐処理を実装する
一度ヒットした攻撃をarrayで管理して、既に存在していたら当たらないといった処理を書いている
これのために signal を追加していて、ちょっと複雑。自分ではもっとシンプルにできないか考えたい
武器側に、既にヒットした敵を管理しておき、それで一回しか当たらないようにできないか?
いや、それも面倒。。。
最終的に以下のコードでいいやとなったkidooom.icon
code:gd
func _on_area_entered(area: Area2D) -> void:
if not area.is_in_group("attack"):
return
var playerAttack := area as PlayerAttackBase
if playerAttack == null:
return
match HurtBoxType:
0: # COOLDOWN
collision.call_deferred("set", "disabled", true)
disableTimer.start()
1: # HIT_ONCE
if hitonce_array.has(playerAttack):
return
hitonce_array.append(playerAttack)
playerAttack.tempdisable()
hurt.emit(playerAttack.damage, playerAttack.angle, playerAttack.knockback_amount)
playerAttack.hit.emit(1)
ヒット時の音を追加
敵死亡時の爆発エフェクトの追加
https://www.youtube.com/watch?v=I8JJl9hgaGU
Part8: Tornado
竜巻攻撃の実装
ぐねぐね動くように、左方向への角度変更、右方向への角度変更成分を生成時に計算し、Tween で繰り返し実行させる
その他は、IceSpear の実装とほぼ同じ。
Player の最後の移動向き: last_movement を保持しておき、それを参照して発射方向を決める
真似して似たような攻撃を実装してみようkidooom.icon
https://gyazo.com/2cdeae478482c57ca2becd710a6d5fab
トルネードのようにグネグネ攻撃するようになった
https://www.youtube.com/watch?v=Q2J3mnqqd5I
Part9: Javelin Attack
画面上に常に残り、定期的に起動して敵に向かって突進するジャベリン攻撃の実装
起動状態なのか休止状態なのかわかりにくいので、違う形状や画像がいいかなと思ったkidooom.icon
参考にするならば、召喚獣とかの攻撃かなkidooom.icon
ちょっと今回のコードは、依存関係や重複処理の観点から気になる点が多かった
Timer 3つが絡みあう処理は追うのも難しくなりそう
https://www.youtube.com/watch?v=LjV8rKWz9hY
Part10: Experience
経験値ジェムを作成する
Area2D ノードに、Sprite2D, CollisionShape2D, AudioStreamPlayer
緑、青、赤の3種類のSpriteを用意し、Scriptで切り替え
経験値の値によって、_ready 時に参照spriteを切り替える
プレイヤーによって収集されたタイミングで、以下を実行する
code:gd
sound.play()
collision.call_deferred("set", "disabled", true)
sprite.visible = false
音の再生終了タイミングで queue_free() させたいので、signal を受信して実行する
code:gd
func _on_snd_collected_finished():
queue_free()
ジェムの初期速度をマイナス値にすることで、プレイヤーに引き寄せる前に一旦逆側にバウンスする動きを作れる
これは別ページに切り出してメモったほうがいいな。TODO:
Player に経験値関連のプロパティを追加する
Player に2つの Area2Dを追加する
一つは、経験値ジェムを引き寄せる状態にする検知エリア
ジェムが on_enter したときに、ジェムの target を player に設定することでジェムが動きだす
もう一つが、引き寄せた経験値ジェムを実際に取得完了にするエリア
ジェムが on_enter したときに、取得処理を実行しつつ、経験値を得る
経験値取得処理
現在レベル、現在経験値、取得経験値の3つのプロパティがあり
現在レベルから、次のレベルに必要な経験値を計算する
現在経験値 + 取得経験値がそれを超えていたら、現在経験値を0にしてレベルアップ処理をする
取得経験値が更に次のレベルアップ必要経験値を超えているかもしれないので、再帰処理をする
経験値バーを追加する
Player ノードに GUI の CanvasLayer を追加するのかkidooom.icon
まあでも確かに、プレイヤーがいる時しか意味のない経験値バーだから、Player 配下に設置してもいいのか
経験値取得時に、value と max_value を更新する
Enemy を倒した時に経験値ジェムを落とすようにする
add_child する時は、call_deferred で同一フレームにしないようにしている
今後自分もこれ注意するかkidooom.icon
参考にして、敵を倒したら 経験値ジェムを落とすようにした(画像は仮)
https://gyazo.com/780e38fa51dad7f01b1d1b44ee367a5d
https://www.youtube.com/watch?v=8nkY5C0HqeQ
Part11: Leveling
レベルアップ時のラベルやパワーアップ選択パネルのGUIを組み立てる
GUI Control ノード配下に、Panel, Label, HBoxContainerなどを作成する
画面外から現れるようにしたいので、Position を x= 800 など画面外になるように初期設定しておき、レベルアップ時にTweenで移動させるように実装している
レベルアップGUIのProcess Mode を When Paused に変更しておき、レベルアップ時に get_tree().paused = true にしてゲームプレイは一時停止にする
パワーアップ選択パネルを新規シーンで作成
ColorRectを親にして
Custom Minimum Size を設定して、シュリンクされないようにする
更にColorRectとSprite2Dを組み合わせてパワーアップサムネイルを構築
Labelを必要な分だけ追加し、位置を調整
name
description
level
Script を作成
signal を追加し、Player の upgrade_character と接続する
パワーアップ選択したことをプレイヤーに通知
マウス選択で signal を発行する
Player のレベルアップ処理で、このパワーアップ選択パネルシーンを instantiate して、BoxContainer 配下に追加する
Player.upgrade_characterでは、アップグレード処理をしつつ、レベルアップ演出を終了させる
get_tree().paused = false にしてゲーム停止を解除する
レベルアップパネルを隠す、移動する
動的に生成したパワーアップパネルを queue_free()する
アップグレード処理自体は、次回の Part12 で
https://www.youtube.com/watch?v=AL9KcZnnsw4
Part12: Upgrades
Playerのアップグレードを管理するため、UpgradeDBという名前でNodeを作成する
AutoLoadに追加する
アップグレードのDictonariyを定義する
名前、説明、アイコンパス、事前に必要なアップグレード、タイプなど
アップグレード選択パネルで ready 時に、UpgradeDBからそのアップグレードの各種情報を読み込んで表示する
プレイヤーのレベルアップ時に、ランダムでアップグレードを抽選する処理を書く
既に取得済みのは無視
習得条件を満たさないものは無視
最終的に見つからない場合は、Foodになるなど余りアイテムを指定しておく(動画ではパワーアップパネル側で設定)
プレイヤーのプロパティに、取得済みのアップグレード配列を追加
他にも、アップグレードに関連するスピードや防御力などを追加
アップグレード選択時に、実際にプレイヤーを強化する
いよいよこのアップグレードシステムが入ったら、ゲームとして良い感じになったと思う
https://www.youtube.com/watch?v=5oFODSYE02I
Part13: Healthbars & more
ヘルスバーの作り方について
この動画では、かなり簡素な作り方
ゲームのカウントダウンタイマーの追加
Label ノードを追加し、毎秒テキストを更新する
この例では、EnemySpawner のタイマーと同期して、シグナルを発行させてプレイヤー側のタイマー表示GUIを更新している
取得済みアップグレードを表示する TextureRect を作成
GridContainerを2つ追加
武器用のContainerと
パッシブのアップグレード用のContainer
重複しないように、取得済みのアップグレードコンテナをGridに追加する
https://www.youtube.com/watch?v=lw-eucpatFg
Partt14: More Enemies
敵キャラクターを追加する回
https://www.youtube.com/watch?v=r577iAHMf84
Part15: Finishing the game
ゲームオーバーパネルを追加する。レベルアップパネルとだいたい同じなので、複製して作成
ゲームで共通に使うボタンノードをブランチ化
ホバーとクリック時の音をつけておく。
なるほど〜。毎回つけるの面倒だし、この共通化は有りだなkidooom.icon
Theme 使えばいいんじゃない?と思うようなこともやっている
死亡時に、ゲームオーバーパネルを表示する
その時、経過時間をチェックしてクリアしたかゲームオーバーかを制御
タイトル画面を作成
Controlノード配下に、ColorRect/Label/自作Buttonを並べる
Playボタン押した時、Worldシーンにchangeするだけ
メインBGMを追加
pause 時に止まらないように、ProcessMode = Always に設定。これは確かに大事kidooom.icon
死んだ時には止まるようにスクリプトを組む
お疲れさまでした。
色々学びがあってありがたい動画シリーズだったkidooom.icon
https://www.youtube.com/watch?v=LZzCMWxg5a4
より改善する案の動画
EnemyBaseのノードを共通化するの良いな、ScriptだけBase化するのやりづらいと思っていたkidooom.icon
ノードまるごとを共通化できないから、子階層に置く前提でブランチ化して、Spriteとかはそこから変更すればいいのか
オーブ全収集アイテムを追加について
オーブ収集範囲を増加パッシブの追加について
どこまで敵を出したらFPSに影響があるのか、最適化を考慮する
敵の最大capを設定している
最大に達したら単純に出さないのではなく、配列にためておいて後で出す。確かに、それでボスとか出なくなったらおかしくなるから必要な処理だ。kidooom.icon
画面に移ってない時は、visible = false にしたら負荷軽減される。なるほどkidooom.icon
ゲームの負荷に合わせて省エネ挙動を制御するFrameSavorノードをWorld配下に追加し、FPSをチェックして制御を加えるのは参考になったkidooom.icon
一部の敵のコリジョンをdisabledにしたり、アニメーションをstopしたり